TCA で依存性を逆転させる
モジュールが分かれてたほうが DIP のメリットを最大限受けれる (ビルド時間等) ので、マルチモジュール前提。
- インターフェイスとなるオブジェクトは
TestDependencyKey
だけに準拠しておく - インターフェイスとなるオブジェクトがあるモジュール内で
DependencyValues
に登録する- これにより実装を知らないでも
Dependency
から取ってこれる
- これにより実装を知らないでも
- 別モジュールとして
liveValue
を提供してDependencyKey
に準拠する - それぞれは別 library (SPM の場合の話) として提供しておき、同じ library には入れないことで複雑な実装が頻繁にビルドされないようにする
- Xcode Project にインターフェイスが入った library と実装 (
liveValue
) が入った library を依存するようにし、 Xcode Target のビルドで初めて依存解決が行われるようにすることで、完全に (Package 内部では) 分離できる。
iso-words の実装を見ればわかる
ApiClient
module と ApiClientLive
module を見るのが一番分かりやすい: Package.swift
ApiClient
がインターフェイスになっていて、 ApiClientLive
にて ApiClient
を extension して liveValue
を実装している。
また、モジュール間の依存関係から見ても DIP できていて、以下の図のようになっている。
flowchart TD xcode[isowords.xcodeproj] --> AppFeature xcode --> ApiClientLive subgraph Swift Package subgraph Core library AppFeature --> ApiClient AppFeature --> OtherFeatures OtherFeatures --> ApiClient end subgraph Live library direction BT ApiClientLive -- add liveValue --> ApiClient end end
ApiClient
はどこにも依存していない- 実際には
SharedModules
や swift-dependencies 等のいくつかのライブラリに依存しているが、置いておく
- 実際には
- 各 Feature (アプリケーション層) は
ApiClient
の軽量なインターフェイスに依存している ApiClientLive
が実際の処理であるliveValue
を提供し、 Infrastructure 層的な役割になっている- Infra は
AppFeature
ライブラリからは見えない liveValue
の在り処を知っているのはisowords.xcodeproj
だけ。- Xcode Project が最終的な DI コンテナ的な役割になっている
- 基本的には
AppFeature
にすべて詰め込まれているので、 Xcode Project への依存もきれい
swift-dependencies の実装から考える
Dependency
として登録するには、 DependencyValues
の subscript で取ってこれるようにする必要があった。
public struct DependencyValues: Sendable {
public subscript<Key: TestDependencyKey>(
key: Key.Type,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line
) -> Key.Value where Key.Value: Sendable { ... }
}
DependencyValues
の subscript としての制約は TestDependencyKey
であるというのが肝。
TestDependencyKey
と DependencyKey
の定義を見る。
public protocol TestDependencyKey {
associatedtype Value: Sendable = Self
static var previewValue: Value { get }
static var testValue: Value { get }
}
public protocol DependencyKey: TestDependencyKey {
static var liveValue: Value { get }
associatedtype Value = Self
static var previewValue: Value { get }
static var testValue: Value { get }
}
つまり以下の流れで分離できるというのが、仕組みからも分かる。
@Dependency(keyPath)
で使えるようにするにはDependencyValues
の subscript に型が適合する必要がある- →
DependencyValues.subscript
の key はTestDependencyKey
である必要がある - →
TestDependencyKey
にはliveValue
(具体的な実装) は不要- よって別モジュールに実装を分けられる
- →
liveValue
については別モジュールにextension
して追加する- このとき合わせて
DependencyKey
に準拠しておく
- このとき合わせて
- → 具体的な実装無しで I/F が独立させることが可能になった